
TL;DR:可接受使用政策(AUP)在稽核時考的是「這台工作站上個月去了哪裡」的資料溯源能力。本文實作兩條分析線,一條是自管邊界 FortiGate 接 VictoriaLogs 做即時 LogsQL 分析與 Prometheus 告警。另一條是針對代管設備匯出檔、具備個資防護的純前端解析器(fortigate-log-viewer)。文內完整記錄 FortiOS 7.6.7 授權過期斷網危機、時區 UTC+8 飄移、日誌時間對帳等一線維運碰到的問題實錄。
可接受使用政策(Acceptable Use Policy, AUP)通常是一份寫在公司文件中的漂亮規章,規範同仁哪些網站能上、哪些業務不能碰。但在外部稽核或內部調查的會議室裡,沒有稽核員會考你這幾條內容,所有人只會盯著你看:
「某台涉案或中毒的工作站,上個月到底連了哪些外部網站?你拿得出客觀證據嗎?」
這個問題的標準答案,就在邊界防火牆上。
在 [Day 22] 中,我利用 FortiGate 的 type="traffic" action="deny" 回答了「誰在敲我的外部節點」(外部資安事件)。今天改用它的 type="utm" subtype="webfilter" 來回答「內網的人從哪台工作站去了哪裡」。這不僅對應資安事件調查,更是 IT 通用控制(ITGC)中「使用者活動紀錄具備可追溯性」的核心要件。
在真實的企業維運中,日誌通常會面臨兩種情境,本文將以雙軌架構並行推進:
這兩條線使用同一套欄位轉換定義,這樣一來,同一份日誌在線上與離線跑出來的統計數字自然分毫不差。
場域驗證設備為自行管理的 FortiGate 60F,韌體版本為 FortiOS 7.6.7。要讓防火牆吐出 WebFilter 日誌,需執行三項變更。變更前務必依 [Day 22] 流程先執行組態備份(POST /api/v2/monitor/system/config/backup):
日誌中的 catdesc(如 Gambling、Phishing、Information Technology)是由 FortiGuard 動態評等給予的。可透過 GUI 的 System → FortiGuard 或 REST API 檢視授權狀態:
GET /api/v2/monitor/license/status
授權過期時的「全網斷網」陷阱
本實測場域 FortiGate 防火牆的 Web Filtering 授權剛好過期。請特別注意:沒有授權時,FortiGate 不是只把類別留白而已!
每一筆日誌都會被標記為eventtype="ftgd_err" level="error" msg="A rating error occurs"。若防護 Profile 沒有啟用set options error-allow,防火牆預設會阻擋所有評等失敗的網站,導致整個內網瞬間斷網!有授權的場域也務必開啟此選項,防範 FortiGuard 雲端服務不可達時發生大範圍意外。
至 Security Profiles → Web Filter 新增 aup-monitor,設定原則為:
Monitor(放行並留存紀錄,產生 action="passthrough")。Block。error-allow)。CLI 實作設定如下:
config webfilter profile
edit "aup-monitor"
config ftgd-wf
set options error-allow
config filters
edit 1
set category 1
next
# ... 各類別預設為 monitor
edit 35
set category 26
set action block
next
# 61 (Phishing), 86 (Spam URLs) 同樣配置為 block
end
end
next
end
config firewall policy
edit 1
set utm-status enable
set ssl-ssh-profile "certificate-inspection"
set webfilter-profile "aup-monitor"
set logtraffic utm
next
end
隱私與安全平衡點:為什麼選擇
certificate-inspection?
certificate-inspection僅分析 TLS Handshake 階段的 SNI(Server Name Indication)與憑證內容,不進行封包解密(No Deep SSL Inspection)。
因此 HTTPS 的日誌紀錄只會呈現完整網域名稱(URL 為https://<hostname>/),不會留下具體網址路徑。這在隱私權保護(AUP)上是刻意的設計——我只需要證明同仁在上班時間去了哪個站點,不應也不需窺探其瀏覽的具體內容。
同時請注意,FortiOS 7.6.7 預設會阻擋憑證失效、過期、低於 TLS 1.1 以及帶有 ECH(Encrypted Client Hello)的連線,此變更會提高連線標準的嚴格程度,需詳列於你在 ISO27001 相關的系統變更單中。
在改完 Policy 後 5 秒鐘,收集端即刻抓到首筆事件:
eventtime=1791389685837697000 tz="+0800" logid="0318012800" type="utm" subtype="webfilter"
eventtype="ftgd_err" level="error" vd="root" policyid=1 policytype="policy" sessionid=12658995
srcip=ws-01 srcport=64018 srcintf="internal" srcintfrole="lan" dstip=203.0.113.134 dstport=80
dstintf="wan1" dstintfrole="wan" proto=6 httpmethod="GET" service="HTTP" hostname="site-168.example"
profile="aup-monitor" action="passthrough" reqtype="direct" url="http://site-168.example/generate_204"
sentbyte=320 rcvdbyte=0 direction="outgoing" msg="A rating error occurs" error="unknown"
變更生效後的第一個小時內,系統共收集到 5,415 筆 WebFilter 紀錄(涵蓋 21 台工作站、315 個外部網站)。
generate_204)一小時便觸發了 2,178 次請求,佔整體 40%。維運者在出報表時必須學會辨識這類機器特徵,然後進行降噪工程。| 欄位名稱 | 範例值 | 維運與稽核用途 |
|---|---|---|
srcip |
192.168.2.23 |
發動連線的工作站 IP(WebFilter 事件不含 srcname,需透過 IP 識別或關聯) |
hostname |
docs.victoriametrics.com |
目標網站名稱。HTTPS 取自 SNI,HTTP 取自 Host 標頭 |
url |
https://docs.victoriametrics.com/ |
存取路徑。在證照檢查模式下,HTTPS 僅保留主機,HTTP 則有完整 URI |
catdesc |
Information Technology |
FortiGuard 分類。授權過期時整欄為空 |
eventtype / action |
ftgd_err / passthrough |
處置動作。阻擋時通常為 ftgd_blk / blocked |
解析大地雷:LogsQL 中的
hostname命名衝突RFC 5424 Syslog 標頭自帶
hostname(代表防火牆主機名fgt-edge),而 FortiGate 訊息內容裡也包含hostname(代表目標網站)。
若直接執行unpack_logfmt,訊息內的網站名稱會覆蓋掉防火牆名稱!
正確寫法:在解析前先執行rename hostname as fgt。
-- 1. 依工作站列出造訪網站排行(過去 7 天)
_time:7d hostname:=fgt-edge "type=\"utm\"" "subtype=\"webfilter\""
| rename hostname as fgt
| unpack_logfmt from _msg fields (srcip, hostname, catdesc, action)
| stats by (srcip, hostname, catdesc) count() as n
| sort by (srcip, n desc)
| limit 500
-- 2. 網站類別存取分布(放行 vs 阻擋)
_time:7d hostname:=fgt-edge "type=\"utm\"" "subtype=\"webfilter\""
| rename hostname as fgt
| unpack_logfmt from _msg fields (catdesc, action)
| stats by (catdesc, action) count() as n
| sort by (n) desc
-- 3. 異常阻擋清單(依工作站與網站分類)
_time:7d hostname:=fgt-edge "subtype=\"webfilter\"" "action=\"blocked\""
| rename hostname as fgt
| unpack_logfmt from _msg fields (srcip, hostname, catdesc, action)
| filter action:=blocked
| stats by (srcip, hostname, catdesc) count() as n
| sort by (n) desc
| limit 50

面對向電信商租賃、無法自訂 Syslog 的代管防火牆,我只能取得管理後台匯出的文字檔。為此,我設計了開源專案 fortigate-log-viewer。
它採用 Vite + React 開發,利用瀏覽器端 JavaScript 直接解析百萬行 Log,完全不需要後端資料庫,更嚴格遵守資安隱私邊界。
舊版本在讀取日誌時,會主動拿連線日誌裡的裸 IP 去連 Google DoH(DNS over HTTPS)查詢 PTR 反查紀錄。
在 v0.1.0 中完成重構:
dns.google)。在解析 FortiOS 日誌時,踩到了經典的瀏覽器時區雷區:
原先程式使用 toISOString().split('T')[0] 預設載入當日資料,這取到的是 UTC 日期,但後續篩選引擎卻以操作者的本地時間(UTC+8)進行比對。
結果導致台灣時間凌晨 00:00 到 08:00 的日誌,在匯入後瞬間被全部過濾掉,畫面永遠顯示「符合條件 0 筆」!經改寫以 Local Time 實體處理時間,並於 CI 流程中強制加入 TZ=Asia/Taipei 自動化測試,徹底封殺此問題。
為杜絕供應鏈風險,Docker 映像檔捨棄過期的 Node 20,升級至 Node 22,並全面以雜湊 Digest 固定版本:
FROM node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 AS builder
FROM nginx:alpine@sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2 AS runner
啟動只需一行,僅綁定本機端點:
docker compose up -d --build # 存取 http://127.0.0.1:5173

線上與離線要能產出一致的報告,關鍵在於跨系統欄位定義的正規化契約:
| 統一目標 | 解析優先權順序 | 設計依據與實務考量 |
|---|---|---|
| 工作站識別 | user $\to$ srcname $\to$ srcip |
有 AD 驗證取帳號,無認證取主機名,皆無則退回 IP 位址。因 WebFilter 日誌不含主機名,本場域最終以 IP 收斂 |
| 目標網站 | hostname $\to$ dstip |
嚴格排除 url 欄位!避免 url="/" 與完整 URI 被切成兩個不同站點 |
| 分類狀態 | catdesc $\to$ 缺值填 Unrated |
當 FortiGuard 授權失效時,統一歸納為未評等,禁止以 Protocol(如 HTTPS)誤填為類別 |
| 阻擋判斷 | action 符合 block / deny / dropped 等關鍵字 |
UTM 放行狀態通常標註為 passthrough,其餘視為阻擋 |
| 基準時間 | 線上取 _time,離線取 eventtime $\to$ date+time+tz |
均轉為微秒級 Epoch 絕對時間軸 |
為驗證統計邏輯一致性,透過自動化比對腳本 aup-compare.sh 處理。它會臨時啟動一個乾淨的 VictoriaLogs 實例,將手動匯出的日誌載入線上端,同時交給離線 Python 引擎分析,最後以 diff 比對產出的 Markdown 資料表。
$ sh firewall/aup-compare.sh fgt-webfilter-2026-10-08.log
fortigate-export-load: 2160 lines, 2160 loaded, 0 skipped (no key=value or no time)
online : ~/day24-aup/raw/compare/online.md
offline: ~/day24-aup/raw/compare/offline.md
aup-compare: SAME (93 table rows)

devname 欄位。線上載入器預設查無主機名而噴出空值,修正方式為在讀取時預設補上 --devname FortiGate。_time 只精準到整秒,而內部 eventtime 為奈秒。兩者量測存在 -1.42s 至 +0.95s 的系統延遲,在查詢時間視窗邊緣容易有 1~3 筆的邊界落差,比對時需在視窗邊界放寬兩秒以求精準。tz="+0800" 解析。2160 解析 + 0 略過,做到百分之百精確對帳。WebFilter 紀錄了員工的數位足跡,屬於高度敏感的個人資料(PII)。收集不是為了監控,而是為了資安防護與事故歸因。
[ 隱私保護邊界 ]
HTTP 流量 ───────( 完整路徑暴露 )───────> [ 協定原罪:難以遮蔽 ]
HTTPS 流量 ───────( 僅截取 SNI FQDN )────> [ 合規推薦:路徑隱匿 ]
certificate-inspection,HTTPS 不解密,僅記錄造訪之主機名,不觸碰個人通訊內容、網址參數及 Payload。一般同仁誤觸無害被擋網站,屬於常態維運,應排程產出週報,不應觸發即時警報(避免警報疲勞)。唯有符合特定異常模式時,才需由 Prometheus 介入告警:
| 規則名稱 | 嚴重層級 | 觸發條件 | 設計意圖 |
|---|---|---|---|
AupUrgentCategoryHit |
Critical | 踩中惡意網址、釣魚站或垃圾郵件分類 $\ge 1$ 筆 | 防火牆雖已攔截,但代表工作站可能已被釣魚攻擊或植入後門,需立即隔離調查 |
AupBlockedBurst |
Warning | 單一主機一小時內遭攔截 $\ge 20$ 次 | 排除人為誤點,通常代表惡意外掛或殭屍程式在持續嘗試對外連線 |
AupSilent |
Info | 連續 6 小時全網 WebFilter 紀錄為 0 筆 | 防呆機制:偵測是否有人誤關 Profile 或 Syslog 連線中斷 |
AupReportStale |
Warning | 背景稽核排程超過 30 分鐘未正常產出 | 靜默失效防護,並連動 Alertmanager Inhibit 抑制其他衍生報警 |
維運技巧:防止 Prometheus 假警報
在實作
AupSilent規則時,當防火牆無日誌送出,LogsQLstats by產出的指標序列會直接消失,導致 PromQL 條件判斷無從成立。
我透過排程回報腳本,明確設定環境變數AUP_EXPECT=fgt-edge,讓腳本在無資料時主動寫入數值0,讓監控系統能確實捕捉到「死寂」狀態。

git clone https://github.com/ivanusto/fortigate-log-viewer
cd fortigate-log-viewer
docker compose up -d --build
# 瀏覽本機端點: http://127.0.0.1:5173
# 1. 驗證收集端已收到第一筆資料
curl -s http://127.0.0.1:9428/select/logsql/query \
-d 'query=_time:1h hostname:=fgt-edge "subtype=\"webfilter\"" | stats count()'
# 2. 測試匯出日誌兩端比對腳本
sh firewall/aup-compare.sh fgt-webfilter-2026-10-08.log
# 3. 執行 Prometheus 規則單元測試
promtool check rules prometheus/rules/aup.yml
promtool test rules tests/rules_aup_test.yml
建立 AUP 的監控不是為了製造不信任,而是為了在資安風暴來臨時,用扎實的系統足跡保護企業與同仁。我實作的方式,儘量用最少量的個資揭露(SNI 檢視),換取了關鍵的追溯能力,並透過線上與離線的雙軌工具,確保不論設備由誰託管,都能得到相同標準的解答。
在今天設定 FortiGate 的過程中,過期的 FortiGuard 授權也差點踩中斷網地雷,而離線工具的 Docker 映像檔也提醒了 Node 20 已經默默步入 EOL。
明天,Day 25 將進入「基礎架構資產的 EOL(生命週期終止)監控」,看看如何把韌體更新、作業系統支援到期日全面轉化為自動化告警指標,不再讓過期組件成為架構中的隱形定時炸彈。
hostname 與訊息內 hostname 的撞名srcip 不該是串流欄位/insert/jsonline、_time_field、_stream_fields
promtool test rules 的 values 展開語法